/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.watch;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewDebug;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * <p>This class can be used to enable the use of HierarchyViewer inside an
 * application. HierarchyViewer is an Android SDK tool that can be used
 * to inspect and debug the user interface of running applications. For
 * security reasons, HierarchyViewer does not work on production builds
 * (for instance phones bought in store.) By using this class, you can
 * make HierarchyViewer work on any device. You must be very careful
 * however to only enable HierarchyViewer when debugging your
 * application.</p>
 *
 * <p>To use this view server, your application must require the INTERNET
 * permission.</p>
 *
 * <p>The recommended way to use this API is to register activities when
 * they are created, and to unregister them when they get destroyed:</p>
 *
 * <pre>
 * public class MyActivity extends Activity {
 *     public void onCreate(Bundle savedInstanceState) {
 *         super.onCreate(savedInstanceState);
 *         // Set content view, etc.
 *         ViewServer.get(this).addWindow(this);
 *     }
 *
 *     public void onDestroy() {
 *         super.onDestroy();
 *         ViewServer.get(this).removeWindow(this);
 *     }
 *
 *     public void onResume() {
 *         super.onResume();
 *         ViewServer.get(this).setFocusedWindow(this);
 *     }
 * }
 * </pre>
 *
 * <p>
 * In a similar fashion, you can use this API with an InputMethodService:
 * </p>
 *
 * <pre>
 * public class MyInputMethodService extends InputMethodService {
 *     public void onCreate() {
 *         super.onCreate();
 *         View decorView = getWindow().getWindow().getDecorView();
 *         String name = "MyInputMethodService";
 *         ViewServer.get(this).addWindow(decorView, name);
 *     }
 *
 *     public void onDestroy() {
 *         super.onDestroy();
 *         View decorView = getWindow().getWindow().getDecorView();
 *         ViewServer.get(this).removeWindow(decorView);
 *     }
 *
 *     public void onStartInput(EditorInfo attribute, boolean restarting) {
 *         super.onStartInput(attribute, restarting);
 *         View decorView = getWindow().getWindow().getDecorView();
 *         ViewServer.get(this).setFocusedWindow(decorView);
 *     }
 * }
 * </pre>
 * 界面优化，用于界面捕获
 */
public class ViewServer implements Runnable {
  /**
   * The default port used to start view servers.
   */
  private static final int VIEW_SERVER_DEFAULT_PORT = 4939;
  private static final int VIEW_SERVER_MAX_CONNECTIONS = 10;
  private static final String BUILD_TYPE_USER = "user";

  // Debug facility
  private static final String LOG_TAG = "ViewServer";

  private static final String VALUE_PROTOCOL_VERSION = "4";
  private static final String VALUE_SERVER_VERSION = "4";

  // Protocol commands
  // Returns the protocol version
  private static final String COMMAND_PROTOCOL_VERSION = "PROTOCOL";
  // Returns the server version
  private static final String COMMAND_SERVER_VERSION = "SERVER";
  // Lists all of the available windows in the system
  private static final String COMMAND_WINDOW_MANAGER_LIST = "LIST";
  // Keeps a connection open and notifies when the list of windows changes
  private static final String COMMAND_WINDOW_MANAGER_AUTOLIST = "AUTOLIST";
  // Returns the focused window
  private static final String COMMAND_WINDOW_MANAGER_GET_FOCUS = "GET_FOCUS";

  private ServerSocket mServer;
  private final int mPort;

  private Thread mThread;
  private ExecutorService mThreadPool;

  private final List<WindowListener> mListeners =
          new CopyOnWriteArrayList<WindowListener>();

  private final HashMap<View, String> mWindows = new HashMap<View, String>();
  private final ReentrantReadWriteLock mWindowsLock = new ReentrantReadWriteLock();

  private View mFocusedWindow;
  private final ReentrantReadWriteLock mFocusLock = new ReentrantReadWriteLock();

  private static ViewServer sServer = new ViewServer(ViewServer.VIEW_SERVER_DEFAULT_PORT);

  /**
   * Returns a unique instance of the ViewServer. This method should only be
   * called from the main thread of your application. The server will have
   * the same lifetime as your process.
   *
   * If your application does not have the <code>android:debuggable</code>
   * flag set in its manifest, the server returned by this method will
   * be a dummy object that does not do anything. This allows you to use
   * the same code in debug and release versions of your application.
   *
   * @param context A Context used to check whether the application is
   *                debuggable, this can be the application context
   */
  public static ViewServer get(Context context) {
    ApplicationInfo info = context.getApplicationInfo();
    if (BUILD_TYPE_USER.equals(Build.TYPE) &&
            (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
      if (sServer == null) {
        sServer = new ViewServer(ViewServer.VIEW_SERVER_DEFAULT_PORT);
      }

      if (!sServer.isRunning()) {
        try {
          sServer.start();
        } catch (IOException e) {
          Log.d(LOG_TAG, "Error:", e);
        }
      }
    } else {
      sServer = new NoopViewServer();
    }

    return sServer;
  }

  private ViewServer() {
    mPort = -1;
  }

  /**
   * Creates a new ViewServer associated with the specified window manager on the
   * specified local port. The server is not started by default.
   *
   * @param port The port for the server to listen to.
   *
   * @see #start()
   */
  private ViewServer(int port) {
    mPort = port;
  }

  /**
   * Starts the server.
   *
   * @return True if the server was successfully created, or false if it already exists.
   * @throws IOException If the server cannot be created.
   *
   * @see #stop()
   * @see #isRunning()
   * @see WindowManagerService#startViewServer(int)
   */
  public boolean start() throws IOException {
    if (mThread != null) {
      return false;
    }

    mThread = new Thread(this, "Local View Server [port=" + mPort + "]");
    mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
    mThread.start();

    return true;
  }

  /**
   * Stops the server.
   *
   * @return True if the server was stopped, false if an error occurred or if the
   *         server wasn't started.
   *
   * @see #start()
   * @see #isRunning()
   * @see WindowManagerService#stopViewServer()
   */
  public boolean stop() {
    if (mThread != null) {
      mThread.interrupt();
      if (mThreadPool != null) {
        try {
          mThreadPool.shutdownNow();
        } catch (SecurityException e) {
          Log.w(LOG_TAG, "Could not stop all view server threads");
        }
      }

      mThreadPool = null;
      mThread = null;

      try {
        mServer.close();
        mServer = null;
        return true;
      } catch (IOException e) {
        Log.w(LOG_TAG, "Could not close the view server");
      }
    }

    mWindowsLock.writeLock().lock();
    try {
      mWindows.clear();
    } finally {
      mWindowsLock.writeLock().unlock();
    }

    mFocusLock.writeLock().lock();
    try {
      mFocusedWindow = null;
    } finally {
      mFocusLock.writeLock().unlock();
    }

    return false;
  }

  /**
   * Indicates whether the server is currently running.
   *
   * @return True if the server is running, false otherwise.
   *
   * @see #start()
   * @see #stop()
   * @see WindowManagerService#isViewServerRunning()
   */
  public boolean isRunning() {
    return mThread != null && mThread.isAlive();
  }

  /**
   * Invoke this method to register a new view hierarchy.
   *
   * @param activity The activity whose view hierarchy/window to register
   *
   * @see #addWindow(View, String)
   * @see #removeWindow(Activity)
   */
  public void addWindow(Activity activity) {
    String name = activity.getTitle().toString();
    if (TextUtils.isEmpty(name)) {
      name = activity.getClass().getCanonicalName() +
              "/0x" + System.identityHashCode(activity);
    } else {
      name += "(" + activity.getClass().getCanonicalName() + ")";
    }
    addWindow(activity.getWindow().getDecorView(), name);
  }

  /**
   * Invoke this method to unregister a view hierarchy.
   *
   * @param activity The activity whose view hierarchy/window to unregister
   *
   * @see #addWindow(Activity)
   * @see #removeWindow(View)
   */
  public void removeWindow(Activity activity) {
    removeWindow(activity.getWindow().getDecorView());
  }

  /**
   * Invoke this method to register a new view hierarchy.
   *
   * @param view A view that belongs to the view hierarchy/window to register
   * @name name The name of the view hierarchy/window to register
   *
   * @see #removeWindow(View)
   */
  public void addWindow(View view, String name) {
    mWindowsLock.writeLock().lock();
    try {
      mWindows.put(view.getRootView(), name);
    } finally {
      mWindowsLock.writeLock().unlock();
    }
    fireWindowsChangedEvent();
  }

  /**
   * Invoke this method to unregister a view hierarchy.
   *
   * @param view A view that belongs to the view hierarchy/window to unregister
   *
   * @see #addWindow(View, String)
   */
  public void removeWindow(View view) {
    View rootView;
    mWindowsLock.writeLock().lock();
    try {
      rootView = view.getRootView();
      mWindows.remove(rootView);
    } finally {
      mWindowsLock.writeLock().unlock();
    }
    mFocusLock.writeLock().lock();
    try {
      if (mFocusedWindow == rootView) {
        mFocusedWindow = null;
      }
    } finally {
      mFocusLock.writeLock().unlock();
    }
    fireWindowsChangedEvent();
  }

  /**
   * Invoke this method to change the currently focused window.
   *
   * @param activity The activity whose view hierarchy/window hasfocus,
   *                 or null to remove focus
   */
  public void setFocusedWindow(Activity activity) {
    setFocusedWindow(activity.getWindow().getDecorView());
  }

  /**
   * Invoke this method to change the currently focused window.
   *
   * @param view A view that belongs to the view hierarchy/window that has focus,
   *             or null to remove focus
   */
  public void setFocusedWindow(View view) {
    mFocusLock.writeLock().lock();
    try {
      mFocusedWindow = view == null ? null : view.getRootView();
    } finally {
      mFocusLock.writeLock().unlock();
    }
    fireFocusChangedEvent();
  }

  /**
   * Main server loop.
   */
  public void run() {
    try {
      mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost());
    } catch (Exception e) {
      Log.w(LOG_TAG, "Starting ServerSocket error: ", e);
    }

    while (mServer != null && Thread.currentThread() == mThread) {
      // Any uncaught exception will crash the system process
      try {
        Socket client = mServer.accept();
        if (mThreadPool != null) {
          mThreadPool.submit(new ViewServerWorker(client));
        } else {
          try {
            client.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      } catch (Exception e) {
        Log.w(LOG_TAG, "Connection error: ", e);
      }
    }
  }

  private static boolean writeValue(Socket client, String value) {
    boolean result;
    BufferedWriter out = null;
    try {
      OutputStream clientStream = client.getOutputStream();
      out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
      out.write(value);
      out.write("\n");
      out.flush();
      result = true;
    } catch (Exception e) {
      result = false;
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
          result = false;
        }
      }
    }
    return result;
  }

  private void fireWindowsChangedEvent() {
    for (WindowListener listener : mListeners) {
      listener.windowsChanged();
    }
  }

  private void fireFocusChangedEvent() {
    for (WindowListener listener : mListeners) {
      listener.focusChanged();
    }
  }

  private void addWindowListener(WindowListener listener) {
    if (!mListeners.contains(listener)) {
      mListeners.add(listener);
    }
  }

  private void removeWindowListener(WindowListener listener) {
    mListeners.remove(listener);
  }

  private interface WindowListener {
    void windowsChanged();
    void focusChanged();
  }

  private static class UncloseableOutputStream extends OutputStream {
    private final OutputStream mStream;

    UncloseableOutputStream(OutputStream stream) {
      mStream = stream;
    }

    public void close() throws IOException {
      // Don't close the stream
    }

    public boolean equals(Object o) {
      return mStream.equals(o);
    }

    public void flush() throws IOException {
      mStream.flush();
    }

    public int hashCode() {
      return mStream.hashCode();
    }

    public String toString() {
      return mStream.toString();
    }

    public void write(byte[] buffer, int offset, int count)
            throws IOException {
      mStream.write(buffer, offset, count);
    }

    public void write(byte[] buffer) throws IOException {
      mStream.write(buffer);
    }

    public void write(int oneByte) throws IOException {
      mStream.write(oneByte);
    }
  }

  private static class NoopViewServer extends ViewServer {
    private NoopViewServer() {
    }

    @Override
    public boolean start() throws IOException {
      return false;
    }

    @Override
    public boolean stop() {
      return false;
    }

    @Override
    public boolean isRunning() {
      return false;
    }

    @Override
    public void addWindow(Activity activity) {
    }

    @Override
    public void removeWindow(Activity activity) {
    }

    @Override
    public void addWindow(View view, String name) {
    }

    @Override
    public void removeWindow(View view) {
    }

    @Override
    public void setFocusedWindow(Activity activity) {
    }

    @Override
    public void setFocusedWindow(View view) {
    }

    @Override
    public void run() {
    }
  }

  private class ViewServerWorker implements Runnable, WindowListener {
    private Socket mClient;
    private boolean mNeedWindowListUpdate;
    private boolean mNeedFocusedWindowUpdate;

    private final Object[] mLock = new Object[0];

    public ViewServerWorker(Socket client) {
      mClient = client;
      mNeedWindowListUpdate = false;
      mNeedFocusedWindowUpdate = false;
    }

    public void run() {
      BufferedReader in = null;
      try {
        in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);

        final String request = in.readLine();

        String command;
        String parameters;

        int index = request.indexOf(' ');
        if (index == -1) {
          command = request;
          parameters = "";
        } else {
          command = request.substring(0, index);
          parameters = request.substring(index + 1);
        }

        boolean result;
        if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) {
          result = writeValue(mClient, VALUE_PROTOCOL_VERSION);
        } else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) {
          result = writeValue(mClient, VALUE_SERVER_VERSION);
        } else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) {
          result = listWindows(mClient);
        } else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
          result = getFocusedWindow(mClient);
        } else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
          result = windowManagerAutolistLoop();
        } else {
          result = windowCommand(mClient, command, parameters);
        }

        if (!result) {
          Log.w(LOG_TAG, "An error occurred with the command: " + command);
        }
      } catch(IOException e) {
        Log.w(LOG_TAG, "Connection error: ", e);
      } finally {
        if (in != null) {
          try {
            in.close();

          } catch (IOException e) {
            e.printStackTrace();
          }
        }
        if (mClient != null) {
          try {
            mClient.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

    private boolean windowCommand(Socket client, String command, String parameters) {
      boolean success = true;
      BufferedWriter out = null;

      try {
        // Find the hash code of the window
        int index = parameters.indexOf(' ');
        if (index == -1) {
          index = parameters.length();
        }
        final String code = parameters.substring(0, index);
        int hashCode = (int) Long.parseLong(code, 16);

        // Extract the command's parameter after the window description
        if (index < parameters.length()) {
          parameters = parameters.substring(index + 1);
        } else {
          parameters = "";
        }

        final View window = findWindow(hashCode);
        if (window == null) {
          return false;
        }

        // call stuff
        final Method dispatch = ViewDebug.class.getDeclaredMethod("dispatchCommand",
                View.class, String.class, String.class, OutputStream.class);
        dispatch.setAccessible(true);
        dispatch.invoke(null, window, command, parameters,
                new UncloseableOutputStream(client.getOutputStream()));

        if (!client.isOutputShutdown()) {
          out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
          out.write("DONE\n");
          out.flush();
        }

      } catch (Exception e) {
        Log.w(LOG_TAG, "Could not send command " + command +
                " with parameters " + parameters, e);
        success = false;
      } finally {
        if (out != null) {
          try {
            out.close();
          } catch (IOException e) {
            success = false;
          }
        }
      }

      return success;
    }

    private View findWindow(int hashCode) {
      if (hashCode == -1) {
        View window = null;
        mWindowsLock.readLock().lock();
        try {
          window = mFocusedWindow;
        } finally {
          mWindowsLock.readLock().unlock();
        }
        return window;
      }


      mWindowsLock.readLock().lock();
      try {
        for (Entry<View, String> entry : mWindows.entrySet()) {
          if (System.identityHashCode(entry.getKey()) == hashCode) {
            return entry.getKey();
          }
        }
      } finally {
        mWindowsLock.readLock().unlock();
      }

      return null;
    }

    private boolean listWindows(Socket client) {
      boolean result = true;
      BufferedWriter out = null;

      try {
        mWindowsLock.readLock().lock();

        OutputStream clientStream = client.getOutputStream();
        out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);

        for (Entry<View, String> entry : mWindows.entrySet()) {
          out.write(Integer.toHexString(System.identityHashCode(entry.getKey())));
          out.write(' ');
          out.append(entry.getValue());
          out.write('\n');
        }

        out.write("DONE.\n");
        out.flush();
      } catch (Exception e) {
        result = false;
      } finally {
        mWindowsLock.readLock().unlock();

        if (out != null) {
          try {
            out.close();
          } catch (IOException e) {
            result = false;
          }
        }
      }

      return result;
    }

    private boolean getFocusedWindow(Socket client) {
      boolean result = true;
      String focusName = null;

      BufferedWriter out = null;
      try {
        OutputStream clientStream = client.getOutputStream();
        out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);

        View focusedWindow = null;

        mFocusLock.readLock().lock();
        try {
          focusedWindow = mFocusedWindow;
        } finally {
          mFocusLock.readLock().unlock();
        }

        if (focusedWindow != null) {
          mWindowsLock.readLock().lock();
          try {
            focusName = mWindows.get(mFocusedWindow);
          } finally {
            mWindowsLock.readLock().unlock();
          }

          out.write(Integer.toHexString(System.identityHashCode(focusedWindow)));
          out.write(' ');
          out.append(focusName);
        }
        out.write('\n');
        out.flush();
      } catch (Exception e) {
        result = false;
      } finally {
        if (out != null) {
          try {
            out.close();
          } catch (IOException e) {
            result = false;
          }
        }
      }

      return result;
    }

    public void windowsChanged() {
      synchronized (mLock) {
        mNeedWindowListUpdate = true;
        mLock.notifyAll();
      }
    }

    public void focusChanged() {
      synchronized (mLock) {
        mNeedFocusedWindowUpdate = true;
        mLock.notifyAll();
      }
    }

    private boolean windowManagerAutolistLoop() {
      addWindowListener(this);
      BufferedWriter out = null;
      try {
        out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream()));
        while (!Thread.interrupted()) {
          boolean needWindowListUpdate = false;
          boolean needFocusedWindowUpdate = false;
          synchronized (mLock) {
            while (!mNeedWindowListUpdate && !mNeedFocusedWindowUpdate) {
              mLock.wait();
            }
            if (mNeedWindowListUpdate) {
              mNeedWindowListUpdate = false;
              needWindowListUpdate = true;
            }
            if (mNeedFocusedWindowUpdate) {
              mNeedFocusedWindowUpdate = false;
              needFocusedWindowUpdate = true;
            }
          }
          if (needWindowListUpdate) {
            out.write("LIST UPDATE\n");
            out.flush();
          }
          if (needFocusedWindowUpdate) {
            out.write("FOCUS UPDATE\n");
            out.flush();
          }
        }
      } catch (Exception e) {
        Log.w(LOG_TAG, "Connection error: ", e);
      } finally {
        if (out != null) {
          try {
            out.close();
          } catch (IOException e) {
            // Ignore
          }
        }
        removeWindowListener(this);
      }
      return true;
    }
  }
}